#include <archive.h>


static char __archi[6] = {'.','a','r','c','h','i'};

archi::archi(bool nquiet)
{
  this->reset_trees= false;
  this->reset_lz77 = false;
  this->print_info = nquiet;
  this->ht_back_dist = new cHuffmanTree<uint16_t>();
  this->ht_length = new cHuffmanTree<uint8_t>();
  this->ht_literal = new cHuffmanTree<uint8_t>();
  this->ht_names = new cHuffmanTree<uint8_t>();
  this->ht_headers = new cHuffmanTree<uint8_t>();
  this->lz77=NULL;
  /* Set bstr to NULL and then call set_defaults method 
     to populate some trees with known values */
  this->bstr = NULL;
  set_defaults();
  this->bstr = new cBitstream(ARCHI_BSTR_SIZE);
}

archi::~archi()
{
  if(this->lz77)
    delete this->lz77;
  
  delete this->ht_back_dist;
  delete this->ht_length;
  delete this->ht_literal;
  delete this->ht_names;
  delete this->ht_headers;
}

void archi::decode_archi()
{
  char _archi[6];
  if(None != this->ht_names->DecodeBlock(&_archi,6,this->bstr) || memcmp(_archi,__archi,6))
  {
      fprintf(stderr,"archi: missing '.archi' preamble");
      exit(1);
  }
}

void archi::encode_archi(bool inc_new)
{
  this->ht_names->EncodeBlock(__archi,6,this->bstr,inc_new);
}

void archi::set_defaults()
{
  this->op = archi_op_nop;
  this->fh_in = NULL;
  this->fh_out = NULL;
  
  this->encode_archi(false);
  this->write_ctl(ARCHI_CTL_DIREND,false);
  this->write_ctl(ARCHI_CTL_END,false);
  
  for(char c = 'a' ; c <= 'z' ; c++)
    this->ht_names->Encode(c,NULL,false);
  for(char c = 'A' ; c <= 'Z' ; c++)
    this->ht_names->Encode(c,NULL,false);
  for(char c = '0' ; c <= '9' ; c++)
    this->ht_names->Encode(c,NULL,false);
  this->ht_names->Encode('.',NULL,false);
  this->ht_names->Encode('_',NULL,false);
  this->ht_names->Encode('-',NULL,false);
}
uint8_t archi::get_symbol_size(uint16_t maxval)
{
  uint8_t ss=0;
  while(maxval)
  {
    maxval >>=1;
    ss++;
  }
  return ss;
}
void archi::set_op(enum archi_op op)
{
    this->op = op;
}
enum archi_op archi::get_op()
{
    return this->op;
}
void archi::set_file_in(char * fname)
{
  if(fname==NULL)
    return;
  FILE * fh = fopen(fname,"r");
  if(fh==NULL)
  {
    perror(fname);
    this->release_files();
    exit(1);
  }
  this->fname_in = fname;   
  this->fh_in = fh;
}
void archi::set_file_out(char * fname)
{ 
  if(fname == NULL)
    return;
  FILE * fh = fopen(fname,"w");
  if(fh == NULL)
  {
    perror(fname);
    this->release_files();
    exit(1);
  }
  this->fname_out=fname;    
  this->fh_out=fh;
}
void archi::release_file_in()
{
  if(this->fh_in != NULL)
    fclose(this->fh_in);
}
void archi::release_file_out()
{
  if(this->fh_out!=NULL)
    fclose(this->fh_out);
}
void archi::release_files()
{
  this->release_file_in();
  this->release_file_out();
}
void archi::set_files(char * f_in,char * f_out)
{
  if(f_in != NULL)
    this->set_file_in(f_in);
  if(f_out != NULL)
    this->set_file_out(f_out);
}
void archi::encode_lz77_code(lz77_code * code)
{
  this->ht_back_dist->Encode(code->backward_distance,this->bstr);
  if(code->backward_distance!=0)
    this->ht_length->Encode(code->lenght,this->bstr);
  this->ht_literal->Encode(code->literal,this->bstr);
}
void archi::clear_trees()
{
  this->ht_back_dist->Reset(); 
  this->ht_length->Reset();
  this->ht_literal->Reset();
}
void archi::read_file_header(struct archi_dirent_header * adh)
{
  if(None!=this->ht_headers->DecodeBlock(adh,sizeof(struct archi_dirent_header),this->bstr))
  {
   fprintf(stderr,"archi: decoding file header\n");
   exit(1); 
  }
}

void archi::write_ctl(uint16_t ctl,bool inc_new)
{
  struct archi_dirent_header adh;
  adh.mode = -1;
  adh.name_len_ctl.ctl=ctl;
  this->ht_headers->EncodeBlock(&adh,sizeof(adh),this->bstr,inc_new);
}

uint16_t archi::get_ctl(struct archi_dirent_header * adh)
{
  if(adh->mode == (mode_t)(-1))
    return adh->name_len_ctl.ctl;
  return ARCHI_CTL_NOP;
}
void archi::s_mkdir(const char * path,mode_t mode)
{
  if(mkdir(path,mode))
  {
    perror(path);
    exit(1);
  }  
  if(print_info)
    printf("Creating dir: %s\n",path);
}

void archi::s_chdir(const char * path)
{
  if(chdir(path))
  {
    perror(path);
    exit(1);
  }  
  if(this->print_info)
    printf("Entering directory: %s\n",path);
}
void archi::write_header()
{
}

void archi::read_header()
{
  
}
bool archi::decode_dir(char * f_in)
{
  struct archi_dirent_header adh;
  while(1)
  {
    this->read_file_header(&adh);
    uint16_t ctl = this->get_ctl(&adh);
    switch(ctl)
    {
      case ARCHI_CTL_NOP:
	break;
      case ARCHI_CTL_DIREND:
      {
	this->dir_up();
	return true;
      }
      case ARCHI_CTL_END:
	return false;
      default:
      {
	fprintf(stderr,"archi: wrong CTL\n");
	exit(1);
      }
    }
    char * fname = new char[adh.name_len_ctl.name_len];
    if(None != this->ht_names->DecodeBlock(fname,adh.name_len_ctl.name_len,this->bstr))
    {
      fprintf(stderr,"archi: decoding name\n");
      exit(1);
    }
    if(S_ISDIR(adh.mode))
    {
      this->s_mkdir(fname,adh.mode);
      this->s_chdir(fname);
      this->decode_dir(NULL);
    }
    else if(S_ISREG(adh.mode))
    {
      FILE * fh = fopen(fname,"w");
      if(fh==NULL)
      {
	perror(fname);
	exit(1);
      }
      if(this->print_info)
	printf("Decoding file: %s\n",fname);
      this->decode_file(fh);
      fclose(fh);
    }
    else
    {
     fprintf(stderr,"archi: decode_dir: wrong file mode\n");
     exit(1);
    }
    
    delete fname;
    if(f_in != NULL)
      return true;
  }
}

void  archi::decode_file(FILE * fh)
{
  cHuffmanItemType itype;
  lz77_code code;
  cWindowBuffer wb(256,1);
  if(this->reset_trees)
    this->clear_trees();
  if(this->reset_lz77)
    this->lz77->Clear();
  while((itype=this->ht_back_dist->Decode(this->bstr,&code.backward_distance)))
  {
    
    if(itype == None || itype == ERROR || itype==Node)
    {
      fprintf(stderr,"archi: decoding file error itype=%d\n",itype);
      exit(1);
    }
    if(itype==EOB)
      break;
    if(code.backward_distance)
    {
	this->ht_length->Decode(this->bstr,&code.lenght);
    }
    else
      code.lenght=0;
    this->ht_literal->Decode(this->bstr,&code.literal);
    this->lz77->Decode(&code,&wb);
    for(int i = 0 ; i < wb.get_window_length();i++)
      fwrite(&wb[i],1,1,fh);
    wb.clear();
  }
}

void archi::dir_up()
{
  if(chdir("..")!=0)
  {
    perror("..");
    exit(1);
  }    
}
void archi::encode(char * f_in[],int n,char * f_out,uint16_t window_size,uint16_t buffer_size,bool reset)
{
  
  if(n<=0)
  {
    fprintf(stderr,"archi: no input file\n");
    exit(1);
  }
  bool f_out_malloc = false;
  if(f_out == NULL)
  {
    if(n==1)
    {
      char * f_in_base = basename(f_in[0]);
      int f_out_len = strlen(f_in_base) + strlen(".archi")+1;
      f_out = (char*)malloc(f_out_len);
      strcpy(f_out,f_in_base);
      strcat(f_out,".archi");
      f_out_malloc=true;
    }
    else
      f_out = (char*)"archi.archi";
  }
  if(this->print_info)
    printf("Output file: %s\n",f_out);
  this->lz77 = new cLZ77(window_size,buffer_size);
  this->ht_length->setSymbolSize(this->get_symbol_size(buffer_size));
  this->ht_back_dist->setSymbolSize(this->get_symbol_size(window_size));
  this->set_file_out(f_out);
  this->bstr->set_fh(this->fh_out);
  
  this->encode_archi();
  
  this->reset_lz77=reset;
  this->reset_trees=reset;
  
  struct archi_header archi_header;
  archi_header.type = TYPE_DEFLATE;
  archi_header.mode=0;
  if(this->reset_trees)
    archi_header.mode |= (1<<MODE_RESET_TREES);
  if(this->reset_lz77)
    archi_header.mode |= (1<<MODE_RESET_LZ77);
  archi_header.window_size = window_size;
  archi_header.buffer_size = buffer_size;
  this->ht_headers->EncodeBlock(&archi_header,sizeof(archi_header),this->bstr);
  
  for(int i = 0 ; i < n ; i++)
    this->encode_dir(f_in[i]);
  
  this->write_ctl(ARCHI_CTL_END);
  
  this->bstr->flush_all();
  this->release_file_out();
//   if(f_out_malloc)
//     free(f_out);
}

void archi::decode(char * f_in,char * f_out)
{
  if(f_in == NULL)
  {
    fprintf(stderr,"archi: no input file\n");
    exit(1);
  }
  bool dir_changed=false;
  this->set_file_in(f_in);
  if(f_out != NULL)
  {
    this->s_mkdir(f_out,S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH);
    this->s_chdir(f_out);
    dir_changed=true;
  }
  this->bstr->set_fh_fill(this->fh_in);
  
  this->decode_archi();
  struct archi_header archi_header;
  if(None != this->ht_headers->DecodeBlock(&archi_header,sizeof(archi_header),this->bstr))
  {
    fprintf(stderr,"archi: decoding header\n");
    exit(1);
  }

  if(archi_header.window_size <= 0)
  {
   fprintf(stderr,"archi: wrong window size (%d)\n",archi_header.window_size);
   exit(1);
  }
  if(archi_header.buffer_size <= 0 || archi_header.buffer_size>archi_header.window_size)
  {
   fprintf(stderr,"archi: wrong buffer size\n");
   exit(1);
  }
  if(archi_header.mode&(1<<MODE_RESET_TREES))
    this->reset_trees=true;
  else
    this->reset_trees=false;
  if(archi_header.mode&(1<<MODE_RESET_LZ77))
    this->reset_lz77=true;
  else
    this->reset_lz77=false;
  this->lz77 = new cLZ77(archi_header.window_size,archi_header.buffer_size);
  this->ht_length->setSymbolSize(this->get_symbol_size(archi_header.buffer_size));
  this->ht_back_dist->setSymbolSize(this->get_symbol_size(archi_header.window_size));
  while(this->decode_dir(f_in));
  
  if(dir_changed)
    this->dir_up();
  
  this->release_file_in();
}
    
void archi::write_file_header(mode_t mode,char * name)
{
  struct archi_dirent_header adh;
  adh.mode=mode;
  adh.name_len_ctl.name_len = strlen(name)+1;
  this->bstr->write_block(&adh,sizeof(adh));
  this->bstr->write_block(name,adh.name_len_ctl.name_len);  
}
void archi::print_ok()
{
  struct winsize ws;
  ioctl(fileno(stdout), TIOCGWINSZ, &ws);
  printf("%*s\n",ws.ws_col,"[OK]\n"/*"[" RED "OK" ENDCOLOR "]"*/);  
}
void archi::encode_dir(char * f_in)
{
  struct stat fstat;
  if(stat(f_in,&fstat)!=0)
  {
      perror(f_in);
      exit(1);
  }
  struct archi_dirent_header adh;
  char * f_in_basename = basename(f_in);
  adh.mode=fstat.st_mode;
  adh.name_len_ctl.name_len = strlen(f_in_basename)+1;
  this->ht_headers->EncodeBlock(&adh,sizeof(adh),this->bstr);
  this->ht_names->EncodeBlock(f_in_basename,adh.name_len_ctl.name_len,this->bstr);
  if(S_ISDIR(fstat.st_mode))
  {
    DIR * dir = opendir(f_in);
    if(dir==NULL)
    {
      perror(f_in);
      exit(1);
    }
    struct dirent * dirent;
    this->s_chdir(f_in);
    while((dirent=readdir(dir)) != NULL)
    {
      if(!strcmp(dirent->d_name,".") || !strcmp(dirent->d_name,".."))
	continue;
      
      this->encode_dir(dirent->d_name);
    }
    adh.mode = -1;
    adh.name_len_ctl.ctl=ARCHI_CTL_DIREND;
    this->ht_headers->EncodeBlock(&adh,sizeof(adh),this->bstr);
    if(this->print_info)
      printf("Leaving directory: %s\n",f_in);
    this->dir_up();
    closedir(dir);
    return;
  }
  if(S_ISREG(fstat.st_mode))
  {
    FILE * fh = fopen(f_in,"r");
    if(fh==NULL)
    {
      perror(f_in);
      exit(1);
    }
    if(this->print_info)
      fprintf(stdout,"Encoding file: %s...\n",f_in);
    this->encode_file(fh);
    fclose(fh);
    return;
  }
  fprintf(stderr,"archi: encode_dir: not file or dir\n");
  exit(1);
}

void archi::encode_file(FILE * fh)
{
  uint8_t buff;
  lz77_code code;
  if(this->reset_trees)
    this->clear_trees();
  if(this->reset_lz77)
    this->lz77->Clear();
  while(fread(&buff,sizeof(buff),1,fh))
  {
    if(this->lz77->Encode(buff,&code))
    {
      this->encode_lz77_code(&code);
    }
  }
  if(this->lz77->Flush(&code))
    this->encode_lz77_code(&code);
  this->ht_back_dist->EncodeCtl(EOB,this->bstr);
  
}